package com.wedimob.slideshare;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.wedimob.slideshare.bo.Container;
import com.wedimob.slideshare.bo.Medium;
import com.wedimob.slideshare.parser.annotation.DateParser;
import com.wedimob.slideshare.parser.annotation.HTMLMapping;
import com.wedimob.slideshare.parser.annotation.HTMLMappings;
import com.wedimob.slideshare.source.Source;

public class Factory {
    static public Source getSource(Document doc) {
        for(Source src : Source.values()) {
            if(doc.baseUri().contains(src.getStrategy().getUrlBase()))
                return src;
        }
        
        return null;
    }
    
    static public URL buildURL(Document doc, String url) {
        if (url == null)
            return null;

        try {
            if (url.startsWith("http"))
                return new URL(url);
            if (url.startsWith("//"))
                return new URL("http:" + url);
            return new URL(getSource(doc).getStrategy().getUrlBase() + url);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static public Calendar parseCalendar(String calendar) {
        if (calendar == null)
            return null;

        // minutes
        int unit = 1;

        if (calendar.contains("hour") || calendar.contains("heure"))
            unit = 60;

        if (calendar.contains("day") || calendar.contains("jour"))
            unit = 24 * 60;

        if (calendar.contains("week") || calendar.contains("semaine"))
            unit = 7 * 24 * 60;

        if (calendar.contains("month") || calendar.contains("mois"))
            unit = 30 * 24 * 60;

        if (calendar.contains("year") || calendar.contains("an"))
            unit = 365 * 24 * 60;

        Calendar result = Calendar.getInstance();

        if (unit == 1) {
            try {
                result.setTime(DateParser.parse(calendar));
                return result;
            } catch (Exception e) {
                try {
                    SimpleDateFormat formatter = new SimpleDateFormat(
                            "EEE MMM d HH:mm:ss Z yyyy", Locale.ENGLISH);
                    result.setTime(formatter.parse(calendar));
                } catch (ParseException e1) {
                    return null;
                }
            }
        }

        Integer quantity = parseInteger(calendar);

        if (quantity == null)
            return null;

        result.add(Calendar.MINUTE, -1 * quantity * unit);

        return result;
    }

    static final private Pattern intPattern = Pattern.compile("-?\\d+");

    static public Integer parseInteger(String number) {
        if (number == null)
            return null;

        number = number.replace(",", "");
        Matcher m = intPattern.matcher(number);
        while (m.find()) {
            return Integer.parseInt(m.group());
        }

        return null;
    }

    static final Pattern doublePattern = Pattern.compile("C=(\\d+\\.\\d+)");

    static private Double parseDouble(String number) {
        if (number == null)
            return null;

        number = number.replace(",", "");
        Matcher m = doublePattern.matcher(number);
        while (m.find()) {
            return Double.parseDouble(m.group());
        }

        return null;
    }

    static private Elements getElements(Document doc, String pattern) {
        if (doc == null)
            return null;

        return doc.select(pattern);
    }

    static private Element getFirstElement(Document doc, String pattern) {
        Elements elemts = getElements(doc, pattern);
        if (elemts == null)
            return null;
        if (elemts.size() >= 1)
            return elemts.first();

        return null;
    }

    static public Document createDocument(Element elemt, String baseUri) {
        Document newDoc = new Document(baseUri);
        newDoc.appendElement(elemt.tagName()).append(elemt.html());
        return newDoc;
    }

    static public String getElementAttribute(Document doc, String tag,
            String attr) {

        if (tag.startsWith("TXT[") && tag.endsWith("]")) {
            String tags[] = tag.split(",");
            tags[0] = tags[0].replace("TXT[", "");
            tags[1] = tags[1].replace("]", "");
            String html = doc.html();
            int idxStart = html.indexOf(tags[0]);
            if (idxStart < 0)
                return null;
            int idxEnd = html.indexOf(tags[1], idxStart + tags[0].length());
            if (idxEnd < 0)
                return null;

            return html.substring(idxStart + tags[0].length(), idxEnd);
        }

        Element elmt = getFirstElement(doc, tag);

        if (elmt == null)
            return null;

        Elements elmts = elmt.select(tag);
        if (elmts == null || elmts.size() == 0)
            return null;

        if ((attr != null) && (attr.length() > 0))
            return elmts.get(0).attr(attr);
        else {
            String result = elmts.get(0).text();
            if (result == null || result.length() == 0)
                result = elmts.get(0).toString();
            return result;
        }
    }

    private static void setValues(Document doc, Container<Object> container, Object srcUrlPattern,
            String separator) {

        Class<?> paramClass = (Class<?>) ((ParameterizedType) container
                .getClass().getGenericSuperclass())
                .getActualTypeArguments()[0];

        Elements elemts = getElements(doc, separator);
        for (Element elemt : elemts) {
            Object element = fromDocument(
                    createDocument(elemt, doc.baseUri()), paramClass,
                    srcUrlPattern);
            container.add(element);
        }
    }

    private static void setValue(Document doc, Object result, Field field,
            String str) {
        try {

            if (field.getType() == String.class) {
                field.set(result, str);
            }

            if (field.getType() == Integer.class) {
                field.set(result, parseInteger(str));
            }

            if (field.getType() == Calendar.class) {
                field.set(result, parseCalendar(str));
            }

            if (field.getType() == URL.class) {
                field.set(result, buildURL(doc, str));
            }

            if (field.getType() == Double.class) {
                field.set(result, parseDouble(str));
            }

            if (field.getType() == Medium.class) {
                field.set(result, new Medium(buildURL(doc, str)));
            }

            if (field.getType().getSuperclass() == Container.class) {
                Annotation classAnnotations[] = field.getDeclaredAnnotations();
                @SuppressWarnings("unchecked")
                Container<Object> container = (Container<Object>) field
                        .getType().newInstance();
                field.set(result, container);
                for (Annotation classAnnotation : classAnnotations) {
                    if (classAnnotation instanceof HTMLMappings || classAnnotation instanceof HTMLMapping) {

                        HTMLMapping mapping = getMatchMapping(classAnnotation, doc.baseUri(), result);
                        if (mapping != null) {
                            setValues(doc, container, result, mapping.tag());
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static class MyHTMLMappingComparable implements Comparator<HTMLMapping> {

        @Override
        public int compare(HTMLMapping o1, HTMLMapping o2) {
            if (o1.priority() != o2.priority())
                return Integer.valueOf(o2.priority()).compareTo(Integer.valueOf(o1.priority()));

            if (o1.version() != o2.version())
                return Integer.valueOf(o2.version()).compareTo(Integer.valueOf(o1.version()));

            if (!o1.url().equals(o2.url()))
                return o2.url().compareTo(o1.url());

            return 0;
        }
    }

    private static HTMLMapping getMatchMapping(Object obj, String docUrl, Object src) {
        try {
            if (!(obj instanceof HTMLMappings) && !(obj instanceof HTMLMapping))
                throw new RuntimeException(obj + " is not HTMLMappings or HTMLMapping type");

            List<HTMLMapping> orderedMapping = new ArrayList<HTMLMapping>();

            if (obj instanceof HTMLMappings)
                orderedMapping.addAll(Arrays.asList(((HTMLMappings) obj).mappings()));
            else
                orderedMapping.add((HTMLMapping) obj);

            Collections.sort(orderedMapping, new MyHTMLMappingComparable());

            String url = null;
            Field target = null;
            boolean isAccessible = false;
            for (HTMLMapping mapping : orderedMapping) {
                url = mapping.url();

                if (url.contains("{") && (src != null)) {
                    int beginIdx = url.indexOf("{");
                    int endIdx = url.indexOf("}", beginIdx + 1);
                    String name = url.substring(beginIdx + 1, endIdx);

                    Field fieldTags[] = src.getClass().getDeclaredFields();
                    for (Field field : fieldTags) {
                        if (field.getName().equals(name)) {
                            target = field;
                            break;
                        }
                    }

                    if (target != null) {
                        isAccessible = target.isAccessible();
                        target.setAccessible(true);
                    }

                    url = url.replace("{" + name + "}", target == null ? ""
                            : (target.get(src) == null ? "" : target.get(src)
                                    .toString()));

                    if (target != null) {
                        target.setAccessible(isAccessible);
                    }
                }

                if (docUrl.contains(url))
                    return mapping;
            }

            return null;
        } catch (Exception e) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T fromDocument(Document document, Class<T> classOfT,
            Object result) {
        try {
            Object initialResult = result;

            if (result == null || result.getClass() != classOfT)
                result = classOfT.newInstance();

            if (document == null)
                return (T) result;

            String docUrl = document.baseUri();

            if (result instanceof Container<?>) {
                for (Annotation annotation : result.getClass().getAnnotations()) {
                    if (annotation instanceof HTMLMappings || annotation instanceof HTMLMapping) {

                        HTMLMapping mapping = getMatchMapping(annotation, docUrl, initialResult);

                        if (mapping != null) {
                            setValues(document, (Container<Object>) result, initialResult, mapping.tag());
                        }
                    }
                }
            }

            Field[] fields = classOfT.getDeclaredFields();
            for (Field field : fields) {

                boolean isAccessible = field.isAccessible();
                field.setAccessible(true);

                if (field.get(result) != null) {
                    field.setAccessible(isAccessible);
                    continue;
                }

                for (Annotation annotation : field.getAnnotations()) {
                    if (annotation instanceof HTMLMappings || annotation instanceof HTMLMapping) {

                        HTMLMapping mapping = getMatchMapping(annotation, docUrl, result);
                        if (mapping != null) {
                            String txt = getElementAttribute(
                                    document, mapping.tag(), mapping.attr());
                            setValue(document, result, field, txt);
                            field.setAccessible(isAccessible);
                            continue;
                        }
                    }
                }
            }

            return (T) result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
